package hyl.core.io;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import hyl.core.MyFun;
import hyl.core.reflect.MyKryo;

/**
 * 实现多线程,断点续传下载<br>
 * 
 * ??? 改动过了需要重新测试 ,没时间测试
 * 
 * @author 阿友 3798955@qq.com
 *
 */
public class MyHttpDown implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 3L;
	// 定义下载资源的路径
	String downpath;
	// 指定所下载的文件的保存位置
	// 保存文件文件名
	String savefilepath;
	// 下载时,临时配置文件
	File savetmpfile;
	int hashcode = 0;
	// 定义需要使用多少线程下载资源,默认两个
	int threadNum = 2;
	// 定义线程下载的对象
	DownThread[] threads;
	// 开始时间戳
	long starttime = 0;
	// 结束时间戳
	long endtime = 0;
	// 下载状态 0 未开始, 1 开始下载, 2下载结束
	int state = 0;
	// 定义下载的文件的总大小
	int fileSize;
	final int buffersize = 16384;
	// 下载方式 默认是2,采用range 方式下载
	int type = 2;
	HashMap<String, String> connconf;
	// 400k以内不用分线程,大于400k分线程下载
	static final int minblocksize = 400000;
	// 如果断线可以重新读取之前下载好的配置
	static final String _下载默认文件夹 = MyPath.getPathOfPro() + "下载/";

	static MyKryo _msl = MyKryo.getInstance(MyHttpDown.class);

	private MyHttpDown(String 下载路径, String 存放路径, File 辅助文件, int 线程数) {
		this.downpath = 下载路径;
		this.savefilepath = 存放路径;
		this.savetmpfile = 辅助文件;
		set线程数(线程数);
	}

	public static MyHttpDown getInstance(String 下载路径, String 存放路径) {
		return getInstance(下载路径, 存放路径, 2);
	}

	public static MyHttpDown getInstance(String 下载路径, String 存放路径, int 线程数) {
		String savepath = null;
		String filename = MyPath.getFileNameOfUrl(下载路径);
		System.out.println(filename);
		if (MyFun.isEmpty(存放路径)) {
			savepath = _下载默认文件夹 + filename;
		} else {
			if (MyPath.getFileNameBeginIndex(存放路径) < 0) {
				savepath = 存放路径 + filename;
			} else {
				savepath = 存放路径;
			}
		}
		String tmp = savepath + "!hyl";
		// 如果临时文件存在,并且长度不为0,表示之前已经下载了部分
		// 那么反序列化后,继续下载
		if (MyFile.exist(tmp)) {
			File file = MyFile.openFile(tmp);
			// 如果未开始就重新开始
			if (file.length() == 0)
				return new MyHttpDown(下载路径, savepath, file, 线程数);
			else {
				return _msl.deserialize(file);
			}
		} else {
			if (MyFile.exist(savepath)) {
				System.out.println("文件已经存在");
				return null;
			}
			File file = MyFile.openFile(tmp);
			return new MyHttpDown(下载路径, savepath, file, 线程数);
		}
	}

	public int getType() {
		return type;
	}

	// 下载方法 2:需要http 的range属性支持 , 方法1:不需要 range属性支持
	public void setType(int type) {
		this.type = type;
	}

	
	public String getDownpath() {
		return downpath;
	}

	public void setDownpath(String downpath) {
		this.downpath = downpath;
	}

	public String getSavefilepath() {
		return savefilepath;
	}

	public void setSavefilepath(String savefilepath) {
		this.savefilepath = savefilepath;
	}

	public File getSavetmpfile() {
		return savetmpfile;
	}

	public void setSavetmpfile(File savetmpfile) {
		this.savetmpfile = savetmpfile;
	}

	public int getHashcode() {
		return hashcode;
	}

	public void setHashcode(int hashcode) {
		this.hashcode = hashcode;
	}

	public int getThreadNum() {
		return threadNum;
	}

	public void setThreadNum(int threadNum) {
		this.threadNum = threadNum;
	}

	public DownThread[] getThreads() {
		return threads;
	}

	public void setThreads(DownThread[] threads) {
		this.threads = threads;
	}

	public long getStarttime() {
		return starttime;
	}

	public void setStarttime(long starttime) {
		this.starttime = starttime;
	}

	public long getEndtime() {
		return endtime;
	}

	public void setEndtime(long endtime) {
		this.endtime = endtime;
	}

	public int getState() {
		return state;
	}

	public void setState(int state) {
		this.state = state;
	}

	public int getFileSize() {
		return fileSize;
	}

	public void setFileSize(int fileSize) {
		this.fileSize = fileSize;
	}

	public HashMap<String, String> getConnconf() {
		return connconf;
	}

	public void setConnconf(HashMap<String, String> connconf) {
		this.connconf = connconf;
	}

	public int getBuffersize() {
		return buffersize;
	}

	private void onError(Exception e) {
		e.printStackTrace();
		_msl.serialize(this, savetmpfile);
	}

	private boolean onFinish() {
		boolean 有文件未下载完成 = false;
		for (int i = 0; i < threads.length; i++) {
			if (!threads[i].finished) {
				有文件未下载完成 = true;
				break;
			}
		}
		if (有文件未下载完成) {
			state = 1;
			_msl.serialize(this, savetmpfile);
			// .out.println(get文件信息());
			return false;
		} else {
			state = 2;
			endtime = (MyFun.getMs() - starttime);
			long times = endtime / 1000;
			System.out.println("耗时(s):" + times);
			MyFile.deleteFile(savetmpfile);
			return true;
		}
	}

	public String get文件信息() {
		StringBuilder sb = new StringBuilder();
		sb.append("{\"文件大小\":").append(fileSize);
		sb.append(",\"下载线程数\":").append(threadNum);
		sb.append(",\r\n\"文件保存地址\":\"").append(savefilepath);
		sb.append("\",\r\n\"文件下载路径\":\"").append(downpath);
		sb.append("\"\r\n,\"文件分块\":[");
		int i = 0;
		if (threads.length > 1)
			for (; i < threads.length - 1; i++) {
				sb.append("\r\n").append(threads[i].toString()).append(",");
			}
		sb.append("\r\n").append(threads[i].toString());
		sb.append("]}");
		return sb.toString();
	}

	// 获取下载的完成百分比
	public double get完成情况() {
		// 统计多条线程已经下载的总大小
		int sumSize = 0;
		if (threadNum > 0) {
			for (int i = 0; i < threadNum; i++) {
				sumSize += threads[i].length;
			}
		}
		// 返回已经完成的百分比
		return sumSize * 1.0 / fileSize;
	}

	// 小于一个线程肯定不行了
	public MyHttpDown set线程数(int num) {
		if (threads != null)
			return this;
		if (num > 1) {
			threadNum = num;
		}
		return this;
	}

	/**
	 * 通过配置,可以限定下载的文件
	 */
	public MyHttpDown set连接参数(HashMap<String, String> conf) {
		if (conf == null) {
			connconf = new HashMap<>();
			connconf.put("Accept",
					"image/gif, image/jpeg, image/pjpeg, image/pjpeg, "
							+ "application/x-shockwave-flash, application/xaml+xml, "
							+ "application/vnd.ms-xpsdocument, application/x-ms-xbap, "
							+ "application/x-ms-application, application/vnd.ms-excel, "
							+ "application/vnd.ms-powerpoint, application/msword, */*");
			connconf.put("Accept-Language", "zh-CN");
			connconf.put("Charset", "UTF-8");
			connconf.put("Connection", "Keep-Alive");
		} else
			connconf = conf;
		return this;
	}

	/**
	 * 如果继续下载,必须
	 */
	public void start() {
		try {
			// 如果已经下载完成,就关闭掉不要重复下载
			if (state == 2)
				return;
			else if (state == 0) {
				URL url = new URL(downpath);
				HttpURLConnection conn = (HttpURLConnection) url.openConnection();
				conn.setConnectTimeout(5000);// 5s
				conn.setRequestMethod("GET");
				if (connconf != null) {
					for (Map.Entry<String, String> item : connconf.entrySet()) {
						conn.setRequestProperty(item.getKey(), item.getValue());
					}
				}
				// 进行连接
				conn.connect();
				// 获得响应码
				int code = conn.getResponseCode();
				if (code == 200 || code == 206) {
					starttime = MyFun.getMs();
					fileSize = conn.getContentLength(); // 得到文件大小
					conn.disconnect();
					if (fileSize > 0) {
						if (!MyFile.exist(savefilepath)) {
							RandomAccessFile file = new RandomAccessFile(savefilepath, "rw");
							// 设置本地文件的大小
							file.setLength(fileSize);
							file.close();
						}
					}
					if (fileSize <= minblocksize) {
						// 初始化threads数组
						threads = new DownThread[1];
						threads[0] = new DownThread(0, fileSize, 0);
					} else {// 至少分两个线程下载
						int startPos = 0;
						int n = fileSize / threadNum;// 平均每个块大小
						int m = fileSize % threadNum;// 余下的几个字节,合并到最后一个块下载
						threads = new DownThread[threadNum];
						// System.out.println("每个块=" + n + ",余下=" + m);
						int k = threadNum - 1;
						for (int i = 0; i < k; i++) {
							// 计算每条线程的下载的开始位置
							startPos = i * n;
							threads[i] = new DownThread(startPos, n, i);
						}
						// 下载最后一个块
						startPos = k * n;
						threads[k] = new DownThread(startPos, m + n, k);
						// 下载最后一个多余的块
					}
				}
			}
			for (DownThread down : threads)
				down.start();
			while (true) {
				if (state == 2) {
					return;
				} else {
					Thread.sleep(1000);
					onFinish();
				}
			}
		} catch (Exception e) {
			onError(e);
		}
	}

	/**
	 * 实现断点续传
	 * 
	 * @author 阿友 3798955@qq.com
	 *
	 */
	private class DownThread extends Thread implements Serializable {
		private static final long serialVersionUID = 1L;
		// 当前线程的下载位置
		long startPos;
		// 定义当前线程负责下载的文件大小
		long currentPartSize;
		// 定义已经该线程已下载的字节数
		long length = 0;
		// 第几个线程 -1 表示单线程
		int _块 = 0;
		// 是否完成下载
		boolean finished = false;

		public void setStartPos(long startPos) {
			this.startPos = startPos;
		}

		public void setCurrentPartSize(long currentPartSize) {
			this.currentPartSize = currentPartSize;
		}

		public void set_块(int _块) {
			this._块 = _块;
		}

		public void setFinished(boolean finished) {
			this.finished = finished;
		}

		public DownThread(long 起始位置, long 下载长度, int 第几个块) throws IOException {
			this.setStartPos(起始位置);
			this.setCurrentPartSize(下载长度);
			this.set_块(第几个块);
			// System.out.println(toString());
		}

		// 块信息
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append("{\"inde\":").append(_块);
			sb.append(",\"开始位置\":").append(startPos);
			sb.append(",\"数据长度\":").append(currentPartSize);
			sb.append(",\"已下载\":").append(length).append("}");
			return sb.toString();
		}

		@SuppressWarnings("unused")
		// 该方法比较慢,但是肯定可以下载
		private void save1(HttpURLConnection conn) throws IOException {
			// 进行连接
			conn.connect();
			int code = conn.getResponseCode();
			if (code == 200 || code == 206) {
				// 每个线程使用一个RandomAccessFile进行下载 ,每次更新写入底层数据
				RandomAccessFile currentPart = new RandomAccessFile(savefilepath, "rws");
				// 定位该线程的下载位置
				currentPart.seek(this.startPos + length);
				InputStream inStream = conn.getInputStream();
				// 跳过startPos个字节，表明该线程只下载自己负责哪部分文件。
				inStream.skip(this.startPos + length);
				byte[] buffer = new byte[buffersize];
				int hasRead = 0;
				// 读取网络数据，并写入本地文件
				while (length < currentPartSize && (hasRead = inStream.read(buffer)) != -1) {
					currentPart.write(buffer, 0, hasRead);
					// 累计该线程下载的总大小
					length += hasRead;
				}
				currentPart.close();
				inStream.close();
			}
		}

		// 该方法快,需要远程支持,有的地方会拦截多线程下载
		private void save2(HttpURLConnection conn) throws IOException {
			conn.setRequestProperty("Range", "bytes=" + startPos + "-" + (startPos + currentPartSize - 1));
			// System.out.println( "bytes=" + startPos + "-" + (startPos+currentPartSize));
			conn.connect();
			int code = conn.getResponseCode();
			if (code == 200 || code == 206) {
				// 每个线程使用一个RandomAccessFile进行下载 ,每次更新写入底层数据
				RandomAccessFile currentPart = new RandomAccessFile(savefilepath, "rws");
				// 定位该线程的下载位置
				currentPart.seek(startPos + length);
				BufferedInputStream bis = new BufferedInputStream(conn.getInputStream());
				bis.skip(length);
				byte[] buf = new byte[buffersize];
				int len = 0;
				while ((len = bis.read(buf, 0, buf.length)) != -1) {
					length += len;
					currentPart.write(buf, 0, len);
				}
				// 回收资源
				currentPart.close();
				bis.close();
			}
		}

		@Override
		public void run() {
			try {
				URL url = new URL(downpath);
				HttpURLConnection conn = (HttpURLConnection) url.openConnection();
				conn.setConnectTimeout(5000);
				conn.setRequestMethod("GET");
				if (type == 1)
					save1(conn);
				else if (type == 2)
					save2(conn);
				conn.disconnect();
				// System.out.println("线程:" + Thread.currentThread().getName());
				setFinished(true);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}
